// Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of touch;

/// Touch Handler. Class that handles all touch events and
/// uses them to interpret higher level gestures and behaviors. TouchEvent is a
/// built in mobile safari type:
/// [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
///
/// Examples of higher level gestures this class is intended to support
/// - click, double click, long click
/// - dragging, swiping, zooming
///
/// Touch Behavior:
///      Use this class to make your elements 'touchable' (see Touchable.dart).
///      Intended to work with all webkit browsers.
///
/// Drag Behavior:
///      Use this class to make your elements 'draggable' (see draggable.js).
///      This behavior will handle all of the required events and report the
///      properties of the drag to you while the touch is happening and at the
///      end of the drag sequence. This behavior will NOT perform the actual
///      dragging (redrawing the element) for you, this responsibility is left to
///      the client code. This behavior contains a workaround for a mobile
///      safari bug where the 'touchend' event is not dispatched when the touch
///      goes past the bottom of the browser window.
///      This is intended to work well in iframes.
///      Intended to work with all webkit browsers, tested only on iPhone 3.x so
///      far.
///
/// Click Behavior:
///      Not yet implemented.
///
/// Zoom Behavior:
///      Not yet implemented.
///
/// Swipe Behavior:
///      Not yet implemented.
class TouchHandler {
  final Touchable _touchable;
  Element _element;

  /// The absolute sum of all touch y deltas. */
  int _totalMoveY;

  /// The absolute sum of all touch x deltas. */
  int _totalMoveX;

  /// A list of tuples where the first item is the horizontal component of a
  /// recent relevant touch and the second item is the touch's time stamp. Old
  /// touches are removed based on the max tracking time and when direction
  /// changes.
  List<int> _recentTouchesX;

  /// A list of tuples where the first item is the vertical component of a
  /// recent relevant touch and the second item is the touch's time stamp. Old
  /// touches are removed based on the max tracking time and when direction
  /// changes.
  List<int> _recentTouchesY;

  // TODO(jacobr): make customizable by passing optional parameters to the
  // TouchHandler constructor.
  /// Minimum movement of touch required to be considered a drag.
  static const _MIN_TRACKING_FOR_DRAG = 2;

  /// The maximum number of ms to track a touch event. After an event is older
  /// than this value, it will be ignored in velocity calculations.
  static const _MAX_TRACKING_TIME = 250;

  /// The maximum number of touches to track. */
  static const _MAX_TRACKING_TOUCHES = 5;

  /// The maximum velocity to return, in pixels per millisecond, that is used to
  /// guard against errors in calculating end velocity of a drag. This is a very
  /// fast drag velocity.
  static const _MAXIMUM_VELOCITY = 5;

  /// The velocity to return, in pixel per millisecond, when the time stamps on
  /// the events are erroneous. The browser can return bad time stamps if the
  /// thread is blocked for the duration of the drag. This is a low velocity to
  /// prevent the content from moving quickly after a slow drag. It is less
  /// jarring if the content moves slowly after a fast drag.
  static const _VELOCITY_FOR_INCORRECT_EVENTS = 1;

  late Draggable _draggable;
  late bool _tracking;
  late bool _dragging;
  late bool _touching;
  late int _startTouchX;
  late int _startTouchY;
  late int _startTime;
  late TouchEvent _lastEvent;
  late int _lastTouchX;
  late int _lastTouchY;
  late int _lastMoveX;
  late int _lastMoveY;
  late int _endTime;
  late int _endTouchX;
  late int _endTouchY;

  TouchHandler(Touchable touchable, [Element? element])
      : _touchable = touchable,
        _totalMoveY = 0,
        _totalMoveX = 0,
        _recentTouchesX = <int>[],
        _recentTouchesY = <int>[],
        // TODO(jmesserly): I don't like having to initialize all booleans here
        // See b/5045736
        _dragging = false,
        _tracking = false,
        _touching = false,
        _element = element ?? touchable.getElement();

  /// Begin tracking the touchable element, it is eligible for dragging.
  void _beginTracking() {
    _tracking = true;
  }

  /// Stop tracking the touchable element, it is no longer dragging.
  void _endTracking() {
    _tracking = false;
    _dragging = false;
    _totalMoveY = 0;
    _totalMoveX = 0;
  }

  /// Correct erroneous velocities by capping the velocity if we think it's too
  /// high, or setting it to a default velocity if know that the event data is
  /// bad. Returns the corrected velocity.
  num _correctVelocity(num velocity) {
    num absVelocity = velocity.abs();
    if (absVelocity > _MAXIMUM_VELOCITY) {
      absVelocity = _recentTouchesY.length < 6
          ? _VELOCITY_FOR_INCORRECT_EVENTS
          : _MAXIMUM_VELOCITY;
    }
    return absVelocity * (velocity < 0 ? -1 : 1);
  }

  /// Start listening for events.
  /// If [capture] is True the TouchHandler should listen during the capture
  /// phase.
  void enable([bool capture = false]) {
    onEnd(e) {
      _onEnd(e.timeStamp.toInt(), e);
    }

    _addEventListeners(_element, (e) {
      _onStart(e as TouchEvent);
    }, (e) {
      _onMove(e as TouchEvent);
    }, onEnd, onEnd, capture);
  }

  /// Get the current horizontal drag delta. Drag delta is defined as the deltaX
  /// of the start touch position and the last touch position.
  int getDragDeltaX() {
    return _lastTouchX - _startTouchX;
  }

  /// Get the current vertical drag delta. Drag delta is defined as the deltaY of
  /// the start touch position and the last touch position.
  int getDragDeltaY() {
    return _lastTouchY - _startTouchY;
  }

  /// Get end velocity of the drag. This method is specific to drag behavior, so
  /// if touch behavior and drag behavior is split then this should go with drag
  /// behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
  /// the difference between endPosition and the oldest recent position, and
  /// deltaTime is the difference between endTime and the oldest recent time
  /// stamp.
  Coordinate getEndVelocity() {
    num velocityX = 0;
    num velocityY = 0;

    if (_recentTouchesX.isNotEmpty) {
      num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]);
      velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX;
    }

    if (_recentTouchesY.isNotEmpty) {
      num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]);
      velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY;
    }
    velocityX = _correctVelocity(velocityX);
    velocityY = _correctVelocity(velocityY);
    return Coordinate(velocityX, velocityY);
  }

  /// Return the touch of the last event.
  Touch _getLastTouch() {
    assert(_lastEvent != null); // Last event not set
    return _lastEvent.touches![0];
  }

  /// Is the touch manager currently tracking touch moves to detect a drag?
  bool isTracking() {
    return _tracking;
  }

  /// Touch end handler.
  void _onEnd(int timeStamp, [TouchEvent? e]) {
    _touching = false;
    _touchable.onTouchEnd();
    if (!_tracking || _draggable == null) {
      return;
    }
    Touch touch = _getLastTouch();
    int clientX = touch.client.x as int;
    int clientY = touch.client.y as int;
    if (_dragging) {
      _endTime = timeStamp;
      _endTouchX = clientX;
      _endTouchY = clientY;
      _recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp);
      _recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp);
      _draggable.onDragEnd();
      if (e != null) {
        e.preventDefault();
      }
      ClickBuster.preventGhostClick(_startTouchX, _startTouchY);
    }
    _endTracking();
  }

  /// Touch move handler.
  void _onMove(TouchEvent e) {
    if (!_tracking || _draggable == null) {
      return;
    }
    final touch = e.touches![0];
    int clientX = touch.client.x as int;
    int clientY = touch.client.y as int;
    int moveX = _lastTouchX - clientX;
    int moveY = _lastTouchY - clientY;
    int timeStamp = e.timeStamp!.toInt();
    _totalMoveX += moveX.abs();
    _totalMoveY += moveY.abs();
    _lastTouchX = clientX;
    _lastTouchY = clientY;
    if (!_dragging &&
        ((_totalMoveY > _MIN_TRACKING_FOR_DRAG && _draggable.verticalEnabled) ||
            (_totalMoveX > _MIN_TRACKING_FOR_DRAG &&
                _draggable.horizontalEnabled))) {
      _dragging = _draggable.onDragStart(e);
      if (!_dragging) {
        _endTracking();
      } else {
        _startTouchX = clientX;
        _startTouchY = clientY;
        _startTime = timeStamp;
      }
    }
    if (_dragging) {
      _draggable.onDragMove();
      _lastEvent = e;
      e.preventDefault();
      _recentTouchesX =
          _removeTouchesInWrongDirection(_recentTouchesX, _lastMoveX, moveX);
      _recentTouchesY =
          _removeTouchesInWrongDirection(_recentTouchesY, _lastMoveY, moveY);
      _recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp);
      _recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp);
      _recentTouchesX.add(clientX);
      _recentTouchesX.add(timeStamp);
      _recentTouchesY.add(clientY);
      _recentTouchesY.add(timeStamp);
    }
    _lastMoveX = moveX;
    _lastMoveY = moveY;
  }

  /// Touch start handler.
  void _onStart(TouchEvent e) {
    if (_touching) {
      return;
    }
    _touching = true;
    if (!_touchable.onTouchStart(e) || _draggable == null) {
      return;
    }
    final touch = e.touches![0];
    _startTouchX = _lastTouchX = touch.client.x as int;
    _startTouchY = _lastTouchY = touch.client.y as int;
    int timeStamp = e.timeStamp!.toInt();
    _startTime = timeStamp;
    // TODO(jacobr): why don't we just clear the lists?
    _recentTouchesX = <int>[];
    _recentTouchesY = <int>[];
    _recentTouchesX.add(touch.client.x as int);
    _recentTouchesX.add(timeStamp);
    _recentTouchesY.add(touch.client.y as int);
    _recentTouchesY.add(timeStamp);
    _lastEvent = e;
    _beginTracking();
  }

  /// Filters the provided recent touches list to remove all touches older than
  /// the max tracking time or the 5th most recent touch.
  /// [recentTouches] specifies a list of tuples where the first item is the x
  /// or y component of the recent touch and the second item is the touch time
  /// stamp. The time of the most recent event is specified by [recentTime].
  List<int> _removeOldTouches(List<int> recentTouches, int recentTime) {
    int count = 0;
    final len = recentTouches.length;
    assert(len % 2 == 0);
    while (count < len &&
            recentTime - recentTouches[count + 1] > _MAX_TRACKING_TIME ||
        (len - count) > _MAX_TRACKING_TOUCHES * 2) {
      count += 2;
    }
    return count == 0 ? recentTouches : _removeFirstN(recentTouches, count);
  }

  static List<int> _removeFirstN(List<int> list, int n) {
    return list.sublist(n);
  }

  /// Filters the provided recent touches list to remove all touches except the
  /// last if the move direction has changed.
  /// [recentTouches] specifies a list of tuples where the first item is the x
  /// or y component of the recent touch and the second item is the touch time
  /// stamp. The x or y component of the most recent move is specified by
  /// [recentMove].
  List<int> _removeTouchesInWrongDirection(
      List<int> recentTouches, int lastMove, int recentMove) {
    if (lastMove != 0 &&
        recentMove != 0 &&
        recentTouches.length > 2 &&
        _xor(lastMove > 0, recentMove > 0)) {
      return _removeFirstN(recentTouches, recentTouches.length - 2);
    }
    return recentTouches;
  }

  // TODO(jacobr): why doesn't bool implement the xor operator directly?
  static bool _xor(bool a, bool b) => a != b;

  /// Reset the touchable element.
  void reset() {
    _endTracking();
    _touching = false;
  }

  /// Call this method to enable drag behavior on a draggable delegate.
  /// The [draggable] object can be the same as the [_touchable] object, they are
  /// assigned to different members to allow for strong typing with interfaces.
  void setDraggable(Draggable draggable) {
    _draggable = draggable;
  }
}
